上一篇我們用了DI的技巧,建立了一個假物件來模擬天氣。
這一篇我們要來談談假物件。我們會將假物件分為Stub與Mock。
Stub:用來模擬外部相依物件的回傳結果。
Mock:用來驗證目標與相依物件的互動。
上一篇的模擬物件StubWeather,模擬外部相依物件回傳結果(晴天或雨天)的物件就是屬於Stub。
下圖說明了被測試物件與Stub的互動。Test
:測試程式。SUT
:System under test,被測試物件。Stub
:假物件,用來模擬外部相依物件的回傳結果。
以上一篇雨傘計價的例子來對照上圖Test
:測試程式,UmbrellaTest.totalPrice_sunnyDay()SUT
:被測試物件,也就是Umebralla.totalPrice()用來計價的方法。Stub
:假天氣物件StubWeather,用來回傳預期是晴天或雨天。
另一種模擬物件的方式叫Mock,Mock則是用來驗證目標與相依物件的互動
。
我們用另一個範例來介紹什麼是Mock,這個範例延續賣雨傘的例子。我們用下訂單這個功能來作示範。
這邊的下訂單,先不討論新增到資料庫等。我們來討論一下寄送Email 給使用者這段。如果你在新增訂單時,會同時發送Mail給使用者。而這個發Mail的function又沒有回傳值讓你驗證是否有成功。那你要怎麼確保新增訂單有真的呼叫寄用Email給使用者的function,並傳入正確的參數。
這裡有一個成功訂單insertOrder的方法,裡面其中一段為寄送Mail給使用者
class Order {
// 成立訂單
fun insertOrder(email: String, quantity:Int, price: Int){
val weather = Weather()
val umbrella = Umbrella()
umbrella.totalPrice(weather, quantity, price)
//新增訂單...(省略)
//寄送Email給使用者
val emailUtil = EmailUtil()
emailUtil.sendCustomer(email)
}
}
我們需要測試成立訂單有沒有發mail。而這個寄送Email的function就是一個外部相依。我們需要驗證是否有與這個外部相依互動。
這個新增訂單的function一樣有相依物件的問題
將EmailUtil類別Extract Interface,新增IEmailUtil介面
interface IEmailUtil {
fun sendCustomer(email: String)
}
class EmailUtil : IEmailUtil {
override fun sendCustomer(email: String) {
//假裝發Email
}
}
把EmailUtil 提出到Constructor並改成介面IEmailUtil,我們又完成了依賴注入
fun insertOrder(email: String, quantity: Int, price: Int, emailUtil: IEmailUtil){
val weather = Weather()
val umbrella = Umbrella()
umbrella.totalPrice(weather, quantity, price)
//結帳...(省略)
//寄送Email給客人
emailUtil.sendCustomer(email)
}
可以來寫測試了,這個測試要測成立訂單有沒有發送mail,測試的重點在於有沒有成功呼叫到emailUtil.sendCustomer(email),這裡的emtailUtil傳入的型別是一個介面,也就是說哪個類別實作IEmailUtil.sendCustomer其實已經不重要了。這個方法相依於介面,而不相依於實體。
新增一個MockEmailUtil,我們要用這個假的EmailUtil來記錄是不是有被呼叫到
class MockEmailUtil :IEmailUtil{
// receiveEmail 用來記錄由sendCustomer傳進來的Email
var receiveEmail:String? = null
override fun sendCustomer(email: String) {
receiveEmail = email
}
}
呼叫order.insertOrder,傳入mockEmailUtil,最後用mockEmailUtil.receiveEmail來驗證order.insert裡是否有呼叫IEmailUtil.setCustomer
@Test
fun testInsertOrder() {
val order = Order()
val mockEmailUtil = MockEmailUtil()
val userEmail = "someMail@gmail.com"
order.insertOrder(userEmail, 1, 200, mockEmailUtil)
//用mockEmailUtil.receiveEmail來驗證order.insert裡是否有呼叫IEmailUtil.setCustomer
Assert.assertEquals(userEmail, mockEmailUtil.receiveEmail)
}
從原本的EmailUtil.sendCustomer,改成相依於介面IEmailUtil.sendCustomer就可以讓發Email這段可被測試囉。
驗證的方式,通常有這3種。
前兩種驗證回傳值、驗證物件狀態的改變,遇到外部相依,通常會使用Stub來輔助驗證回傳值
與驗證物件狀態的改變
。驗證目標與相依物件的互動,指的就是Mock。請儘量將互動測試作為你的最後選擇,你應該儘量使用驗證回傳值或驗證物件狀態,因為互動測試會讓測試變的複雜。如果一個測試只測一件事,就只能有一個Mock。如果一個測試存在多個Mock,代表你正在測試多件事情。
範例下載:
https://github.com/evanchen76/TDD_MockSample
參考
單元測試的藝術
30天快速上手TDD Unit Test - Stub, Mock, Fake 簡介
小技巧
Ctrl + R 執行上一個測試
Ctrl + Shift + R 執行目前的測試
下一篇會介紹隔離框架,這些框架會讓你在Mock或Stub變得很簡單。
Android TDD 測試驅動開發:從 UnitTest、TDD 到 DevOps 實踐